// Copyright 2017 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <lib/zx/event.h>

#include <dispatcher-pool/dispatcher-execution-domain.h>
#include <dispatcher-pool/dispatcher-thread-pool.h>

#include <utility>

namespace dispatcher {

// static
fbl::RefPtr<ExecutionDomain> ExecutionDomain::Create(zx::profile profile) {
  zx::event evt;
  if (zx::event::create(0, &evt) != ZX_OK)
    return nullptr;

  if (evt.signal(0u, ZX_USER_SIGNAL_0) != ZX_OK)
    return nullptr;

  fbl::RefPtr<ThreadPool> thread_pool;
  zx_status_t res = ThreadPool::Get(&thread_pool, std::move(profile));
  if (res != ZX_OK)
    return nullptr;
  ZX_DEBUG_ASSERT(thread_pool != nullptr);

  fbl::AllocChecker ac;
  auto new_domain = fbl::AdoptRef(new (&ac) ExecutionDomain(thread_pool, std::move(evt)));
  if (!ac.check())
    return nullptr;

  res = thread_pool->AddDomainToPool(new_domain);
  if (res != ZX_OK)
    return nullptr;

  return new_domain;
}

ExecutionDomain::ExecutionDomain(fbl::RefPtr<ThreadPool> thread_pool, zx::event dispatch_idle_evt)
    : deactivated_(0),
      thread_pool_(std::move(thread_pool)),
      dispatch_idle_evt_(std::move(dispatch_idle_evt)) {
  ZX_DEBUG_ASSERT(thread_pool_ != nullptr);
  ZX_DEBUG_ASSERT(dispatch_idle_evt_.is_valid());
}

ExecutionDomain::~ExecutionDomain() {
  // Assert that the Owner implementation properly deactivated itself
  // before destructing.
  ZX_DEBUG_ASSERT(deactivated());
  ZX_DEBUG_ASSERT(sources_.is_empty());
  ZX_DEBUG_ASSERT(!thread_pool_node_state_.InContainer());
}

void ExecutionDomain::Deactivate(bool sync_dispatch) {
  // Flag ourselves as deactivated.  This will prevent any new event sources
  // from being added to the sources_ list.  We can then swap the contents of
  // the sources_ list with a temp list, leave the lock and deactivate all of
  // the sources at our leisure.
  fbl::DoublyLinkedList<fbl::RefPtr<EventSource>, EventSource::SourcesListTraits> to_deactivate;
  bool sync_needed = false;

  {
    fbl::AutoLock sources_lock(&sources_lock_);
    if (deactivated()) {
      ZX_DEBUG_ASSERT(sources_.is_empty());
    } else {
      deactivated_.store(1u);
      to_deactivate.swap(sources_);
    }

    // If there are dispatch operations currently in flight, clear the
    // dispatch idle event and set the flag indicating to the dispatch
    // operation that it needs to set the event when it finishes.
    if (dispatch_in_progress_) {
      sync_needed = true;
      if (!dispatch_sync_in_progress_) {
        __UNUSED zx_status_t res;
        dispatch_sync_in_progress_ = true;
        res = dispatch_idle_evt_.signal(ZX_USER_SIGNAL_0, 0u);
        ZX_DEBUG_ASSERT(res == ZX_OK);
      }
    }
  }

  // Now deactivate all of our event sources and release all of our references.
  if (!to_deactivate.is_empty()) {
    for (auto& source : to_deactivate) {
      source.Deactivate();
    }
    to_deactivate.clear();
  }

  // Synchronize if needed
  if (sync_needed && sync_dispatch) {
    __UNUSED zx_status_t res;
    zx_signals_t pending;

    res = dispatch_idle_evt_.wait_one(ZX_USER_SIGNAL_0, zx::deadline_after(zx::sec(5)), &pending);

    ZX_DEBUG_ASSERT(res == ZX_OK);
    ZX_DEBUG_ASSERT((pending & ZX_USER_SIGNAL_0) != 0);
  }

  // Finally, exit our thread pool and release our reference to it.
  decltype(thread_pool_) pool;
  {
    fbl::AutoLock sources_lock(&sources_lock_);
    pool = std::move(thread_pool_);
  }

  if (pool != nullptr)
    pool->RemoveDomainFromPool(this);
}

fbl::RefPtr<ThreadPool> ExecutionDomain::GetThreadPool() {
  fbl::AutoLock sources_lock(&sources_lock_);
  return fbl::RefPtr<ThreadPool>(thread_pool_);
}

zx_status_t ExecutionDomain::AddEventSource(fbl::RefPtr<EventSource>&& event_source) {
  if (event_source == nullptr)
    return ZX_ERR_INVALID_ARGS;

  // This check is a bit sketchy...  This event_source should *never* be in
  // any ExecutionDomain's event_source list at this point in time, however if
  // it is, we don't really know what lock we need to obtain to make this
  // observation atomically.  That said, the check will not mutate any state,
  // so it should be safe.  It just might not catch a bad situation which
  // should never happen.
  ZX_DEBUG_ASSERT(!event_source->InExecutionDomain());

  // If this ExecutionDomain has become deactivated, then it is not accepting
  // any new event sources.  Fail the request to add this event_source.
  fbl::AutoLock sources_lock(&sources_lock_);
  if (deactivated())
    return ZX_ERR_BAD_STATE;

  // We are still active.  Transfer the reference to this event_source to our set
  // of sources.
  sources_.push_front(std::move(event_source));
  return ZX_OK;
}

void ExecutionDomain::RemoveEventSource(EventSource* event_source) {
  fbl::AutoLock sources_lock(&sources_lock_);

  // Has this ExecutionDomain become deactivated?  If so, then this
  // event_source may still be on a list (the local 'to_deactivate' list in
  // Deactivate), but it is not in the ExecutionDomain's sources_ list, so
  // there is nothing to do here.
  if (deactivated()) {
    ZX_DEBUG_ASSERT(sources_.is_empty());
    return;
  }

  // If the event_source has not already been removed from the domain's list, do
  // so now.
  if (event_source->InExecutionDomain())
    sources_.erase(*event_source);
}

bool ExecutionDomain::AddPendingWork(EventSource* event_source) {
  ZX_DEBUG_ASSERT(event_source != nullptr);
  ZX_DEBUG_ASSERT(!event_source->InPendingList());
  ZX_DEBUG_ASSERT(event_source->dispatch_state() == DispatchState::WaitingOnPort);

  // If this ExecutionDomain has become deactivated, then it is not accepting
  // any new pending work.   Do not add the source to the pending work queue,
  // and do not tell the caller that it should be processing the queue when we
  // return.  The event source is now in the Idle state.
  fbl::AutoLock sources_lock(&sources_lock_);
  if (deactivated()) {
    event_source->dispatch_state_ = DispatchState::Idle;
    return false;
  }

  // Add this event source to the back of the pending work queue, and tell the
  // caller whether or not it is responsible for processing the queue.
  bool ret = !dispatch_in_progress_;
  if (ret) {
    ZX_DEBUG_ASSERT(pending_work_.is_empty());
    dispatch_in_progress_ = true;
  }

  event_source->dispatch_state_ = DispatchState::DispatchPending;
  pending_work_.push_back(fbl::WrapRefPtr(event_source));

  return ret;
}

bool ExecutionDomain::RemovePendingWork(EventSource* event_source) {
  ZX_DEBUG_ASSERT(event_source != nullptr);

  fbl::AutoLock sources_lock(&sources_lock_);
  if (!event_source->InPendingList())
    return false;

  // If we were on the pending list, then our state must be DispatchPending;
  ZX_DEBUG_ASSERT(event_source->dispatch_state() == DispatchState::DispatchPending);
  pending_work_.erase(*event_source);
  return true;
}

void ExecutionDomain::DispatchPendingWork() {
  // While we have work waiting in the pending queue, dispatch it.
  //
  // TODO(johngro) : To prevent starvation issues, we should probably only
  // perform a finite amount of work, and unwind out into the port wait
  // operation to give other event source owners a chance if this ends up
  // going on for too long.
  while (true) {
    // Enter the sources lock and take a reference to the front of the
    // pending queue.  If the pending work queue is empty, or we have been
    // deactivated, we are finished.
    fbl::RefPtr<EventSource> source;
    {
      fbl::AutoLock sources_lock(&sources_lock_);
      ZX_DEBUG_ASSERT(dispatch_in_progress_);
      if (deactivated() || pending_work_.is_empty()) {
        // Clear the dispatch in progress flag.  If someone is
        // attempting to synchronize with dispatch operations in flight,
        // set the event indicating that we are now idle.
        //
        // Do _not_ clear the pending_work_ queue at this point.  Either
        // the queue is already empty, or we are in the process of
        // deactivating.  If there are items in the pending work queue,
        // then they will be removed by Deactivate as we shut down.
        //
        // If we clear the queue here instead, we will violate the
        // invariant that an EventSource's state is DispatchPending
        // if-and-only-if it is also in its domain's pending_work_queue_
        // (which can cause ASSERTs in other places to fire).
        dispatch_in_progress_ = false;
        if (dispatch_sync_in_progress_) {
          __UNUSED zx_status_t res;
          res = dispatch_idle_evt_.signal(0u, ZX_USER_SIGNAL_0);
          ZX_DEBUG_ASSERT(res == ZX_OK);
        }
        return;
      }

      source = pending_work_.begin().CopyPointer();
    }

    // Attempt to transition to the Dispatching state.  If this fails, it
    // means that we were canceled after we left the sources_lock_ but
    // before we managed to re-enter both the EventSource's object lock and
    // the execution domain's sources lock.  If this is the case, just move
    // on to the next pending source.
    ZX_DEBUG_ASSERT(source != nullptr);
    if (source->BeginDispatching())
      source->Dispatch(this);
  }
}

}  // namespace dispatcher
